package br.ufrj.dcc.ad.metrics;

import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

import org.apache.log4j.Logger;

import br.ufrj.dcc.ad.args.ParametrosSimulacao;
import br.ufrj.dcc.ad.enums.NomeMetrica;
import br.ufrj.dcc.ad.log.LoggerFactory;

public class SystemMetrics {
	
	public static final String ROUND_SEPARATOR = "RODADA", VALUE_SEPARATOR = ",";
//	private static final double piSobre2 = Math.PI / 2.0;
	
	private static int numeroClientesSistema = 0;
	private static int numeroClientesFinalizados = 0;
	private static List<List<Metrica>> temposTotaisNoSistema = new ArrayList<List<Metrica>>();
	private static List<Metrica> temposTotais = new ArrayList<Metrica>();
	private static List<List<Metrica>> temposFilas = new ArrayList<List<Metrica>>();
	private static List<List<Metrica>> temposServidor = new ArrayList<List<Metrica>>();
	private static List<List<Metrica>> temposVaosServidor = new ArrayList<List<Metrica>>();
	private static List<Metrica> duracaoRodadas;
	private static List<MetricasGrupais> metricasGrupais = new ArrayList<MetricasGrupais>();
	private static double ultimoClock, mediaNumeroClientesNoSistema, mediaTempoTransiente;
	private static double ultimaMediaClientesNoSistema;
	
	/**
	 * Usados para o calculo iterativo e a logica estacionária
	 * semelhanca conta quantas vezes a quantidade de clientes esta parecida com a media
	 */
	private static Integer iteracao;
	
	private static Logger loggerMetricas = LoggerFactory.getLogger(LoggerFactory.Type.METRICAS_SIMULACAO);
	private static Logger loggerMetricasIteracao = LoggerFactory.getLogger(LoggerFactory.Type.METRICAS_ITERACAO);
	private static Logger loggerMetricasClientesIteracao = LoggerFactory.getLogger(LoggerFactory.Type.METRICAS_CLIENTES_ITERACAO);
	private static Logger loggerMediana = LoggerFactory.getLogger(LoggerFactory.Type.MEDIANA);
	private static Logger loggerDebug = LoggerFactory.getLogger(LoggerFactory.Type.DEBUG);
	
	/**
	 * Este atributo guarda o tempo médio total no sistema da última iteração.
	 */
	private static List<Double> mediaTempoFila, mediaTempoServidor;
	
	static {
		mediaTempoServidor = new ArrayList<Double>();
		mediaTempoFila = new ArrayList<Double>();
		duracaoRodadas = new ArrayList<Metrica>();
		iteracao = 0;
		ultimaMediaClientesNoSistema = ultimoClock = mediaNumeroClientesNoSistema = mediaTempoTransiente = 0.0;
	}
	
	/**
	 * Guarda todas as métricas temporais de um cliente.
	 * @param tempoServidor O tempo que o cliente ficou no servidor.
	 * @param tempoFilas O tempo que o cliente ficou em cada fila.
	 * @param list 
	 */
	public static void addMetricasSistema(List<Metrica> temposServidor, List<Metrica> temposVaos, List<Metrica> tempoFilas){
		addTemposServidor(temposServidor);
		addTemposFilas(tempoFilas);
		addTemposVaos(temposVaos);
		addTemposTotaisSistema(temposServidor, temposVaos,tempoFilas);
		addTempoTotal(temposServidor, temposVaos,tempoFilas);
	} 

	/**
	 * Adiciona as métricas grupais da iteração:
	 * Média, IC Média, Variância e IC Variância, das métricas individuais:
	 * Tempo no Servidor, Tempo de espera em cada fila e Tempo total do cliente no sistema.
	 */
	public static void calculaMetricasRodada() {
		int i = -1;
		double media;
		for(List<Metrica> temposConsumidos:SystemMetrics.temposServidor){
			media = calculaMedia(temposConsumidos);
			addMetricaGrupal(NomeMetrica.TEMPOSERVIDOR,++i,media,calculaVariancia(temposConsumidos,media));
			mediaTempoServidor.add(media);
		}
		i = -1;
		
		for(List<Metrica> temposVaos:SystemMetrics.temposVaosServidor){
			media = calculaMedia(temposVaos);
			addMetricaGrupal(NomeMetrica.TEMPOVAO,++i,media, calculaVariancia(temposVaos,media));
		}
		
		i = -1;
		for(List<Metrica> temposFila:SystemMetrics.temposFilas){
			media = calculaMedia(temposFila);
			addMetricaGrupal(NomeMetrica.TEMPOFILA,++i,media, calculaVariancia(temposFila,media));
			mediaTempoFila.add(media);
		}
		i = -1;
		for(List<Metrica> tempoTotais:SystemMetrics.temposTotaisNoSistema){
			media = calculaMedia(tempoTotais);
			addMetricaGrupal(NomeMetrica.TEMPOTOTALSISTEMA,++i,media,calculaVariancia(tempoTotais,media));
		}
		media = calculaMedia(temposTotais);
		addMetricaGrupal(NomeMetrica.TEMPOTOTALSISTEMA, -1, media, calculaVariancia(temposTotais, media));
	}
	/**
	 * Gera o arquivo que servirá para gerar a CDF do tempo total no sistema da iteração.
	 */
	public static void printMetricasRodada() {
		double mediana = 0.0;
		int i = 0;
		int tamanho = temposTotais.size();
		Collections.sort(temposTotais);
		mediana = calculaMediana(temposTotais);
		if(!temposTotais.isEmpty()) {
			loggerMetricasIteracao.info(ROUND_SEPARATOR);
			for(Metrica m : temposTotais) {
				i++;
				loggerMetricasIteracao.info(m.getValor() + VALUE_SEPARATOR + (double)i/tamanho);
			}
		}
		loggerMediana.info(mediana);
	}
	
	private static double calculaMediana(List<Metrica> temposTotais) {
		double mediana1,mediana2;
		int tamanho = temposTotais.size();
		if(tamanho != 0) {
		//Se o número de amostras forem par...
			if(tamanho%2 == 0){
				mediana1 = temposTotais.get(tamanho/2).getValor();
				mediana2 = temposTotais.get(tamanho/2 + 1).getValor();
			}else{
				mediana1 = mediana2 = temposTotais.get(tamanho/2 - 1).getValor();
			}
			return (mediana1 + mediana2)/2;
		}
		return -1.0;
	}

	public static void clearMetricasRodada() {
		SystemMetrics.clearMetricas();
		numeroClientesSistema = 0;
		mediaTempoFila.clear();
		mediaTempoServidor.clear();
		iteracao = 0;
		ultimaMediaClientesNoSistema = ultimoClock = mediaNumeroClientesNoSistema = 0.0;
	}
	
	
	
	public static void printSeparatorLogClientes() {
		loggerMetricasClientesIteracao.info(ROUND_SEPARATOR);
		loggerDebug.info(ROUND_SEPARATOR);
	}
	/**
	 * Imprime aw métricas finais da simulação.
	 */
	public static void printMetricasFinais() {
		for(MetricasGrupais metrica: metricasGrupais){
			metrica.printAll(loggerMetricas); 
		}
		
		if(! duracaoRodadas.isEmpty()) {
			StringBuffer buffer = new StringBuffer();
			buffer.append("Duracao das rodadas: ");
			for(Metrica metrica : duracaoRodadas) {
				buffer.append(metrica.getValor() + VALUE_SEPARATOR);
			}
			loggerMetricas.info(buffer.toString());
		}
		
		loggerMetricas.info("Duracao media do estado transiente: " + Double.toString(mediaTempoTransiente));
	}
	
	public static void addDuracaoRodada(Double duracao) {
		duracaoRodadas.add(new DuracaoRodada(duracao));
	}
	
	/**
	 * Limpa as métricas acumuladas para iniciar a próxima iteração.
	 * Não apaga as métricas grupais pois essas precisão ser guardadas para estimar os intervalos de confiança.
	 * no fim da execução.
	 */
	public static void clearMetricas() {
		temposServidor.clear();
		temposVaosServidor.clear();
		temposFilas.clear();
		temposTotaisNoSistema.clear();
		temposTotais.clear();
		numeroClientesFinalizados = 0;
	}
	
	private static void addTemposServidor(List<Metrica> temposServidos){
		int i = -1;
		if(temposServidor.isEmpty()){
			for(i=0;i<temposServidos.size();i++){
				temposServidor.add(new ArrayList<Metrica>());
			}
		}
		i = -1;
		for(List<Metrica> tempoServidoI:temposServidor){
			tempoServidoI.add(temposServidos.get(++i));
		}
	}
	
	private static void addTemposFilas(List<Metrica> temposFila){
		int i = -1;
		if(temposFilas.isEmpty()){
			for(i=0;i<temposFila.size();i++){
				temposFilas.add(new ArrayList<Metrica>());
			}
		}
		i = -1;
		for(List<Metrica> temposFilaI:temposFilas){
			temposFilaI.add(temposFila.get(++i));
		}
	}
	
	private static void addTemposVaos(List<Metrica> temposVaos) {
		int i = -1;
		if(temposVaosServidor.isEmpty()){
			for(i=0;i<temposVaos.size();i++){
				temposVaosServidor.add(new ArrayList<Metrica>());
			}
		}
		i = -1;
		for(List<Metrica> temposVaosI:temposVaosServidor){
			temposVaosI.add(temposVaos.get(++i));
		}
	}
	
	private static void addTemposTotaisSistema(List<Metrica> temposServidos,List<Metrica> temposVaos, List<Metrica> temposFilas){
		int i = -1;
		if(temposTotaisNoSistema.isEmpty()){
			for(i=0;i<temposServidos.size();i++){
				temposTotaisNoSistema.add(new ArrayList<Metrica>());
			}
		}
		i = -1;
		for(List<Metrica> tempoTotalI:temposTotaisNoSistema){
			i++;
			tempoTotalI.add(calculaTempoTotalSistema(temposServidos.get(i),temposVaos.get(i),temposFilas.get(i)));
		}
	}
	
	private static void addTempoTotal(List<Metrica> temposServidor, List<Metrica> temposVaos, List<Metrica> tempoFilas) {
		double valor = 0.0;
		for(Metrica metrica:temposServidor){
			valor += metrica.getValor(); 
		}
		for(Metrica metrica:temposVaos){
			valor += metrica.getValor();
		}
		for(Metrica metrica:tempoFilas){
			valor += metrica.getValor(); 
		}
		temposTotais.add(new TempoTotalSistema(valor));
	}
	
	private static void addMetricaGrupal(NomeMetrica nomeMetrica, int indice, double media,double variancia){
		boolean existeInstancia = false;
		
		for(MetricasGrupais metricaGrupal:metricasGrupais){
			if(metricaGrupal.getNomeMetrica() == nomeMetrica && 
			   indice == metricaGrupal.getIndice()){
				
				metricaGrupal.addMetricasIteracao(media,variancia);
				existeInstancia = true;
			}
		}
		
		if(!existeInstancia) {
			
			List<Double> medias = new ArrayList<Double>();
			medias.add(media);
			List<Double> variancias = new ArrayList<Double>();
			variancias.add(variancia);
			MetricasGrupais metricasGrupais = new MetricasGrupais(nomeMetrica,indice,medias,variancias);
			SystemMetrics.metricasGrupais.add(metricasGrupais);
		}
	}
	
	private static TempoTotalSistema calculaTempoTotalSistema(
			Metrica tempoServido, Metrica tempoVao, Metrica tempoFila) {
		
		return new TempoTotalSistema((tempoServido.getValor() + tempoVao.getValor() + tempoFila.getValor()));
	}
	
	private static double calculaMedia(List<Metrica> metricas){
		double soma = 0.0;
		for(Metrica metrica: metricas){
			soma += metrica.getValor();
		}
		return soma/metricas.size();
	}
	
	private static double calculaVariancia(List<Metrica> metricas,double media){
		double soma = 0.0;
		for(Metrica metrica:metricas){
			soma += Math.pow((metrica.getValor() - media), 2);
		}
		if(metricas.size() > 1){
			return soma/(metricas.size()-1);
		}
		return 0.0;
	}
	
	public static List<Metrica> getTemposTotais() {
		return temposTotais;
	}
	
	public static void setTemposTotais(List<Metrica> temposTotais) {
		SystemMetrics.temposTotais = temposTotais;
	}
	
	// TODO: Introduzir uma lógica para dizer se as métricas estão em estado estacionário.
	public static boolean isStationary(double clockAtual) {
		double porcentagemEstacionario = getPorcentagemEstacionario(clockAtual);
		
		if(clockAtual < ParametrosSimulacao.TEMPO_MINIMO_ESTACIONARIO) {
			return false;
		}
		
		return porcentagemEstacionario > ParametrosSimulacao.porcentagemEstabilizacao;
	}
	
	public static double atualizaMediaTempoTransiente(double tempoTransiente, int rodada) {
		return mediaTempoTransiente = mediaIterativa(mediaTempoTransiente, tempoTransiente, rodada);
	}
	
	public static double getPorcentagemEstacionario(double clockAtual) {
		if(ultimoClock == 0) {
			return 1.0;
		}
		// Clock atual vai ser sempre maior ou igual que o ultimoClock, na pior das hipóteses vai ser igual.
		double div = ultimoClock / clockAtual;
		return 1 - div;
	}
	
	/***
	 * @see http://www.heikohoffmann.de/htmlthesis/node134.html
	 * @param clock
	 * @param clientes
	 */
	public static void atualizaMediaNumeroClientesSistema(double clock, int clientes) {
		double mediaAntiga = mediaNumeroClientesNoSistema;
		double inclinacao = 0.0;
		
		mediaNumeroClientesNoSistema = mediaIterativa(mediaNumeroClientesNoSistema, clientes, iteracao++);
		
		inclinacao = derivada(ultimaMediaClientesNoSistema, mediaNumeroClientesNoSistema);
		
		loggerMetricasClientesIteracao.info(
			Double.toString(clock) + VALUE_SEPARATOR +
			Double.toString(mediaNumeroClientesNoSistema) // + VALUE_SEPARATOR +
//			Integer.toString(clientes) + VALUE_SEPARATOR +
//			Double.toString(inclinacao) + VALUE_SEPARATOR +
//			Double.toString(ultimoClock) + VALUE_SEPARATOR +
//			Double.toString(getPorcentagemEstacionario(clock))
		);
		
		loggerDebug.info(
				"clock: " +
				Double.toString(clock) + VALUE_SEPARATOR +
				"media: " +
				Double.toString(mediaNumeroClientesNoSistema)  + VALUE_SEPARATOR +
				"cli: " +
				Integer.toString(clientes) + VALUE_SEPARATOR +
				"inc: " +
				Double.toString(inclinacao) + VALUE_SEPARATOR +
				"last med: " +
				Double.toString(ultimaMediaClientesNoSistema) + VALUE_SEPARATOR +
				"last clk: " +
				Double.toString(ultimoClock) + VALUE_SEPARATOR +
				"perc: " +
				Double.toString(getPorcentagemEstacionario(clock))
			);
		
		// Se o número de clientes nao for semelhante, reseta o ultimoClock
		if(inclinacao >= ParametrosSimulacao.porcentagemSemelhanca) {
			ultimoClock = clock;
			ultimaMediaClientesNoSistema = mediaAntiga;
		}
	}
	
	public static double mediaIterativa(double mediaAntiga, int valor, int rodada) {
		return mediaAntiga + (valor - mediaAntiga) / (rodada + 1);
	}
	
	public static double mediaIterativa(double mediaAntiga, double valor, int rodada) {
		return mediaAntiga + (valor - mediaAntiga) / (rodada + 1);
	}
	
	/**
	 * Calcular a inclinacao da reta que liga os dois pontos.
	 */
	private static double derivada(double media, double valorAtual) {
		double sub = Math.abs(media - valorAtual) / media;
		if(sub == 0.0) {
			return 0.0;
		}
		/*
		 * Como o domínio da funca atan é (-piSobre2, piSobre2) e eu quero um valor entre 0 e 1.
		 * Talvez nao seja uma boa ideia dividir por dx, pois se pontos estiverem muito longe e a diferenca for
		 * grande o arco tangente seria pequeno do mesmo jeito.
		 * 
		 * A teoria diz que deve ser dividido por delta Y, mas isso implica que
		 * se passar muito tempo, pode haver explosões e o ângulo fica no limite do aceitável
		 * Como o sistema em tese está em estado estacionário, isso não deve acontecer.
		 */
//		return Math.abs(Math.atan(sub / (ultimoClock - clock)) / piSobre2);
		return sub * ParametrosSimulacao.reduceFactor;
//		return Math.abs(Math.atan(sub) / piSobre2);
	}

	public static int getNumeroClientesSistema() {
		return numeroClientesSistema;
	}

	public static void setNumeroClientesSistema(int numeroClientesSistema) {
		SystemMetrics.numeroClientesSistema = numeroClientesSistema;
	}
	
	public static void trocarLogClientes(Integer i) throws IOException {
		LoggerFactory.trocarLog(loggerMetricasClientesIteracao, i);
	}
	
	public static double getMediaNumeroClientesSistema() {
		return SystemMetrics.mediaNumeroClientesNoSistema;
	}

	public static int getNumeroClientesFinalizados() {
		return numeroClientesFinalizados;
	}

	public static void setNumeroClientesFinalizados(
			int numeroClientesFinalizados) {
		SystemMetrics.numeroClientesFinalizados = numeroClientesFinalizados;
	}
}
